
%%%-----------------------------------------------------------------
%%% @doc Handles fetching packages from the remote repository and 
%%%      placing them in the erlware repo.
%%% 
%%% Types:
%%%  @type repo() = string(). Contains address and repo designation. 
%%%   Example: http://www.erlware.org/stable   
%%%  @type timeout() = integer() | infinity. Timeouts are specified in milliseconds.
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(fax_manage).

%%--------------------------------------------------------------------
%% Include files
%%--------------------------------------------------------------------
-include("eunit.hrl").
-include("faxien.hrl").


%%--------------------------------------------------------------------
%% External exports
%%--------------------------------------------------------------------
-export([
	 upgrade_applications/2,
	 upgrade_application/3,
	 upgrade_releases/3,
	 upgrade_release/4,
	 add_repo_to_publish_to/2,
	 remove_repo_to_publish_to/2,
	 add_repo_to_fetch_from/2,
	 remove_repo_to_fetch_from/2,
	 set_request_timeout/2,
	 list/4,
	 describe_app/4,
	 describe_app/3,
	 installed/2,
	 installed/1,
	 remove_release/3, 
	 remove_release/4,
	 remove_app/3, 
	 remove_app/2
	]).

%%--------------------------------------------------------------------
%% Macros
%%--------------------------------------------------------------------

%%--------------------------------------------------------------------
%% Records
%%--------------------------------------------------------------------

%%====================================================================
%% External functions
%%====================================================================


%%--------------------------------------------------------------------
%% @doc 
%%  Fetch the description for a particular application.
%% @spec describe_app(Repos, TargetErtsVsn, AppName, AppVsn, VsnThreshold) -> ok | {error, Reason} | exit()
%%  where
%%   Repos = list()
%%   TargetErtsVsn = string()
%%   AppName = string()
%%   AppVsn = string()
%% @end
%%--------------------------------------------------------------------
describe_app(Repos, TargetErtsVsn, AppName, "LATEST") ->
    describe_app(Repos, TargetErtsVsn, AppName, "LATEST", infinity);

describe_app(Repos, TargetErtsVsn, AppName, AppVsn) ->
    ok = fax_util:foreach_erts_vsn(TargetErtsVsn, 
				   fun(ErtsVsn) -> 
					   Suffix = ewr_util:gen_metadata_suffix(ErtsVsn, AppName, AppVsn),
					   fax_util:do_until(
					     fun(Repo) ->
						     %% TODO - move this path into ewr_util
						     Url = Repo ++ Suffix ++ AppName ++ ".app",
						     ?INFO_MSG("consulting the url ~s~n", [Url]),
						     case ewr_util:consult_url(Url) of
							 {ok, {application, _, Terms}} -> 
							     io:format("~nDescribing Application: ~s~n~n ~p~n", 
								       [AppName, Terms]);
							 Error ->
							     ?ERROR_MSG("consulting the url ~s returns ~p~n", [Url, Error]),
							     Error
						     end
					     end, ok, Repos)
				   end).

%% @spec describe_app(Repos, TargetErtsVsn, AppName) -> ok
%% @equiv describe_app(Repos, TargetErtsVsn, AppName, "LATEST")
describe_app(Repos, TargetErtsVsn, AppName) ->
    describe_app(Repos, TargetErtsVsn, AppName, "LATEST").

%%--------------------------------------------------------------------
%% @doc Add a repository to fetch from. 
%% @spec add_repo_to_fetch_from(Repo, ConfigFilePath) -> ok | {error, Reason}
%%  where
%%   Repos = string()
%%   ConfigFilePath = string()
%% @end
%%--------------------------------------------------------------------
add_repo_to_fetch_from(Repo, ConfigFilePath) ->
    case gas:get_env(faxien, repos_to_fetch_from, undefined) of
	{ok, undefined} -> 
	    {error, no_such_config_entry};
	{ok, Repos} ->
	    gas:modify_config_file(ConfigFilePath, faxien, repos_to_fetch_from, [Repo|Repos])
    end.

%%--------------------------------------------------------------------
%% @doc Remove a repository to fetch from. 
%% @spec remove_repo_to_fetch_from(Repo, ConfigFilePath) -> ok
%%  where
%%   Repos = string()
%%   ConfigFilePath = string()
%% @end
%%--------------------------------------------------------------------
remove_repo_to_fetch_from(Repo, ConfigFilePath) ->
    case gas:get_env(faxien, repos_to_fetch_from, undefined) of
	{ok, undefined} -> 
	    {error, no_such_config_entry};
	{ok, Repos} ->
	    gas:modify_config_file(ConfigFilePath, faxien, repos_to_fetch_from, lists:delete(Repo, Repos))
    end.

%%--------------------------------------------------------------------
%% @doc Add a repository to publish to. 
%% @spec add_repo_to_publish_to(Repo, ConfigFilePath) -> ok | {error, Reason}
%%  where
%%   Repos = string()
%%   ConfigFilePath = string()
%% @end
%%--------------------------------------------------------------------
add_repo_to_publish_to(Repo, ConfigFilePath) ->
    case gas:get_env(faxien, repos_to_publish_to, undefined) of
	{ok, undefined} -> 
	    {error, no_such_config_entry};
	{ok, Repos} ->
	    gas:modify_config_file(ConfigFilePath, faxien, repos_to_publish_to, [Repo|Repos])
    end.

%%--------------------------------------------------------------------
%% @doc Remove a repository to publish to. 
%% @spec remove_repo_to_publish_to(Repo, ConfigFilePath) -> ok
%%  where
%%   Repos = string()
%%   ConfigFilePath = string()
%% @end
%%--------------------------------------------------------------------
remove_repo_to_publish_to(Repo, ConfigFilePath) ->
    case gas:get_env(faxien, repos_to_publish_to, undefined) of
	{ok, undefined} -> 
	    {error, no_such_config_entry};
	{ok, Repos} ->
	    gas:modify_config_file(ConfigFilePath, faxien, repos_to_publish_to, lists:delete(Repo, Repos))
    end.

%%--------------------------------------------------------------------
%% @doc Set the request timeout.
%% @spec set_request_timeout(Timeout, ConfigFilePath) -> ok | {error, Reason}
%%  where
%%   Timeout = timeout()
%%   ConfigFilePath = string()
%% @end
%%--------------------------------------------------------------------
%% TODO set up the gas functions so that they will insert a config entry if none exists.
set_request_timeout(Timeout, ConfigFilePath) ->
    case gas:get_env(faxien, request_timeout, undefined) of
	{ok, undefined} -> 
	    {error, no_such_config_entry};
	{ok, OldTimeout} ->
	    ?INFO_MSG("changing request_timeout from ~s to ~s~n", [OldTimeout, Timeout]),
	    gas:modify_config_file(ConfigFilePath, faxien, request_timeout, Timeout)
    end.

%%--------------------------------------------------------------------
%% @doc upgrade all applications on the install path.
%% @spec upgrade_releases(Repos, InstallationPath) -> ok | {error, Reason}
%%  where
%%   Repos = [string()]
%%   InstallationPath = string()
%% @end
%%--------------------------------------------------------------------
upgrade_releases(Repos, InstallationPath, IsLocalBoot) -> 
    Releases = fax_paths:list_releases(InstallationPath),
    lists:foreach(fun(ReleaseName) -> upgrade_release(Repos, ReleaseName, InstallationPath, IsLocalBoot) end, Releases).

%%--------------------------------------------------------------------
%% @doc upgrade a single release.
%% @spec upgrade_release(Repos, ReleaseName, InstallationPath) -> ok | {error, Reason}
%%  where
%%   Repos = [string()]
%%   ReleaseName = string()
%%   InstallationPath = string()
%% @end
%%--------------------------------------------------------------------
upgrade_release(Repos, ReleaseName, InstallationPath, IsLocalBoot) -> 
    ?INFO_MSG("fax_manage:upgrade_release(~p, ~p, ~p)~n", [Repos, ReleaseName, InstallationPath]),
    case fax_paths:list_release_vsns(InstallationPath, ReleaseName) of
	Vsns when length(Vsns) > 0 ->
	    HighestLocalVsn  = hd(lists:sort(fun(A, B) -> ewr_util:is_version_greater(A, B) end, Vsns)),
	    {ok, {Repo, HighestRemoteVsn}} = fax_util:find_highest_vsn(Repos, ReleaseName, releases),
	    ?INFO_MSG("upgrade_application local versions found ~p and highest remote vsn ~p~n", 
				  [Vsns, HighestRemoteVsn]),
	    case ewr_util:is_version_greater(HighestLocalVsn, HighestRemoteVsn) of
		true ->
		    io:format("~s at version ~s is up to date~n", [ReleaseName, HighestLocalVsn]),
		    ok;
		false when HighestRemoteVsn == HighestLocalVsn ->
		    io:format("~s at version ~s is up to date~n", [ReleaseName, HighestLocalVsn]),
		    ok;
		false ->
		    io:format("upgrading from version ~s of ~s to version ~s~n", [HighestLocalVsn, ReleaseName, HighestRemoteVsn]),
		    fax_install:install_remote_release([Repo], ReleaseName, HighestRemoteVsn, InstallationPath, IsLocalBoot)
	    end;
	[] ->
	    io:format("~s is not present at any version on the system, try installing it first~n", [ReleaseName]), 
	    {error, app_not_installed};
	Error ->
	    io:format("upgrade error for ~s: ~p~n", [ReleaseName, Error]),
	    {error, Error}
    end.
    
%%--------------------------------------------------------------------
%% @doc upgrade all applications on the install path.
%% @spec upgrade_applications(Repos, InstallationPath) -> ok | {error, Reason}
%%  where
%%   Repos = [string()]
%%   InstallationPath = string()
%% @end
%%--------------------------------------------------------------------
upgrade_applications(Repos, InstallationPath) -> 
    AppNames = fax_paths:list_apps(InstallationPath),
    lists:foreach(fun(AppName) -> upgrade_application(Repos, AppName, InstallationPath) end, AppNames).

%%--------------------------------------------------------------------
%% @doc upgrade a single application.
%% @spec upgrade_application(Repos, AppName, InstallationPath) -> ok | {error, Reason}
%%  where
%%   Repos = [string()]
%%   AppName = string()
%%   InstallationPath = string()
%% @end
%%--------------------------------------------------------------------
upgrade_application(Repos, AppName, InstallationPath) -> 
    case fax_paths:list_app_vsns(InstallationPath, AppName) of
	Vsns when length(Vsns) > 0 ->
	    HighestLocalVsn  = hd(lists:sort(fun(A, B) -> ewr_util:is_version_greater(A, B) end, Vsns)),
	    {ok, {Repo, HighestRemoteVsn}} = fax_util:find_highest_vsn(Repos, AppName, lib),
	    ?INFO_MSG("upgrade_application local versions found ~p and highest remote vsn ~p~n", 
				  [Vsns, HighestRemoteVsn]),
	    case ewr_util:is_version_greater(HighestLocalVsn, HighestRemoteVsn) of
		true ->
		    io:format("~s at version ~s is up to date~n", [AppName, HighestLocalVsn]),
		    ok;
		false when HighestRemoteVsn == HighestLocalVsn ->
		    io:format("~s at version ~s is up to date~n", [AppName, HighestLocalVsn]),
		    ok;
		false ->
		    io:format("upgrading from version ~s of ~s to version ~s~n", [HighestLocalVsn, AppName, HighestRemoteVsn]),
		    fax_install:install_remote_application([Repo], AppName, HighestRemoteVsn, InstallationPath)
	    end;
	[] ->
	    io:format("~s is not present on the system, try installing it first~n", [AppName]), 
	    {error, app_not_installed};
	Error ->
	    io:format("upgrade error for ~s: ~p~n", [AppName, Error]),
	    {error, Error}
    end.
    
%%--------------------------------------------------------------------
%% @doc 
%%  Returns a list of packages.
%% @spec list(Repos, Side, SearchType, SearchString) -> string()
%%  where
%%   Repos = list()
%%   Side = lib | releases | both
%%   SearchType = regexp | normal
%%   SearchString = string()
%% @end
%%--------------------------------------------------------------------
list(Repos, Side, SearchType, SearchString) -> 
    FilterFun = case SearchType of
		    regexp ->
			fun(E) -> case regexp:match(E, SearchString) of {match, _, _} -> true; _ -> false end end;
		    normal ->
			fun(E) -> case regexp:match(E, ".*" ++ SearchString ++ ".*") of {match, _, _} -> true; _ -> false end end;
		    Invalid ->
			exit({"Not a valid search type, try normal or regexp", Invalid})
		end,

    case Side of
	both ->
	    Lib      = filter(FilterFun, lists:foldl(fun({_, A}, Acc) -> A ++ Acc end, [], raw_list(Repos, lib))),
	    Releases = filter(FilterFun, lists:foldl(fun({_, A}, Acc) -> A ++ Acc end, [], raw_list(Repos, releases))),
	    print_list(releases, Releases),
	    print_list(lib, Lib);
	Side ->
	    List = filter(FilterFun, lists:foldl(fun({_, A}, Acc) -> A ++ Acc end, [], raw_list(Repos, Side))),
	    print_list(Side, List)
    end.


%%--------------------------------------------------------------------
%% @doc 
%%  Returns a list of packages that are currently installed.
%% @spec installed(InstallationPath, Side) -> string()
%%  where
%%   Side = lib | releases | both
%% @end
%%--------------------------------------------------------------------
installed(InstallationPath, both) ->
    installed(InstallationPath, releases),
    installed(InstallationPath, lib);
installed(InstallationPath, lib) ->
    LibDir       = fax_paths:installed_app_path(InstallationPath),
    Paths        = filelib:wildcard(LibDir ++ "/*"),
    NameVsnPairs = name_and_vsn(Paths),
    io:format("~nInstalled OTP Applications:)~n"),
    lists:foreach(fun({Name, Vsn}) -> io:format("~s   ~s~n", [Name, Vsn]) end, NameVsnPairs);
installed(InstallationPath, releases) ->
    RelDir       = fax_paths:installed_release_path(InstallationPath),
    Paths        = filelib:wildcard(RelDir ++ "/*/*"),
    NameVsnPairs = name_and_vsn(Paths),
    io:format("~nInstalled OTP Releases:~n"),
    lists:foreach(fun({Name, Vsn}) -> io:format("~s   ~s~n", [Name, Vsn]) end, NameVsnPairs).

%% @equiv installed(InstallationPath, both)
%% @spec installed(InstallationPath) -> string()
installed(InstallationPath) ->
    installed(InstallationPath, both).
    
name_and_vsn(Paths) ->
    lists:foldl(fun(Path, Acc) ->
			case fax_paths:package_dir_to_name_and_vsn(Path) of
			    {ok, {Name, Vsn}} -> [{Name, Vsn}|Acc];
			    _                 -> Acc
			end
		end, [], Paths).


%%--------------------------------------------------------------------
%% @doc 
%%  Remove an installed application.
%% @spec remove_app(InstallationPath, AppName, AppVsn) -> ok
%%  where
%%   AppName = string()
%%   AppVsn = string()
%% @end
%%--------------------------------------------------------------------
remove_app(InstallationPath, AppName, AppVsn) ->
    AppPath = fax_paths:installed_app_path(InstallationPath, AppName, AppVsn),
    ewl_file:delete_dir(AppPath).

%%--------------------------------------------------------------------
%% @doc 
%%  Remove all versions of an installed application.
%% @spec remove_app(InstallationPath, AppName) -> ok
%%  where
%%   AppName = string()
%% @end
%%--------------------------------------------------------------------
remove_app(InstallationPath, AppName) ->
    AppVsns = fax_paths:list_app_vsns(InstallationPath, AppName),
    lists:foreach(fun(AppVsn) -> remove_app(InstallationPath, AppName, AppVsn) end, AppVsns).
			  
%%--------------------------------------------------------------------
%% @doc 
%%  Remove an installed release.
%% @spec remove_release(InstallationPath, RelName, RelVsn, Force) -> ok 
%%  where
%%   RelName = string()
%%   RelVsn = string()
%%   Force = bool()
%% @end
%%--------------------------------------------------------------------
remove_release(InstallationPath, RelName, RelVsn, Force) ->
    UniqueSpecs = find_unique_app_specs(InstallationPath, RelName, RelVsn),
    io:format("unique specs ~p~n", [UniqueSpecs]),
    case Force of
	true ->
	    blast(UniqueSpecs, InstallationPath, RelName, RelVsn);
	false ->
	    case question_removal(UniqueSpecs) of
		true ->
		    blast(UniqueSpecs, InstallationPath, RelName, RelVsn);
		false ->
		    ok
	    end
    end.

blast(UniqueSpecs, InstallationPath, RelName, RelVsn) ->
    lists:foreach(fun({AppName, AppVsn}) -> remove_app(InstallationPath, atom_to_list(AppName), AppVsn) end, UniqueSpecs),
    AppPath = fax_paths:installed_release_path(InstallationPath, RelName, RelVsn),
    ewl_file:delete_dir(AppPath).
    
			  

%%--------------------------------------------------------------------
%% @doc 
%%  Remove all versions of an installed release.
%% @spec remove_release(InstallationPath, RelName, Force) -> ok
%%  where
%%   RelName = string()
%% @end
%%--------------------------------------------------------------------
remove_release(InstallationPath, RelName, Force) ->
    RelVsns = fax_paths:list_release_vsns(InstallationPath, RelName),
    lists:foreach(fun(RelVsn) -> remove_release(InstallationPath, RelName, RelVsn, Force) end, RelVsns).
    
    

%%====================================================================
%% Internal functions
%%====================================================================
question_removal(UniqueSpecs) ->
    case ewl_talk:ask([lists:flatten(io_lib:fwrite("Do you want to delete the following apps ~p and the release? [yes|no]", 
						   [UniqueSpecs]))]) of
	Yes when Yes == $y; Yes == $Y; Yes == "yes" ->
	    true;
	No when No == $n; No == $N; No == "no" ->
	    false;
	Error ->
	    ?INFO_MSG("user entered \"~p\"~n", [Error]),
	    io:format("Please enter \"yes\" or \"no\"~n"),
	    question_removal(UniqueSpecs)
    end.
    


find_unique_app_specs(InstallationPath, RelName, RelVsn) ->
    {ok, TargetSpecs} = fetch_app_specs(InstallationPath, RelName, RelVsn),
    RelVsns           = lists:delete(RelVsn, fax_paths:list_release_vsns(InstallationPath, RelName)),
    ReleaseNames      = fax_paths:list_releases(InstallationPath),
    ReleaseTuples = 
	[
	 {RelName, RelVsns}|
	 lists:keydelete(RelName, 1, 
			 lists:map(fun(ReleaseName) ->
					   ReleaseVersions = fax_paths:list_release_vsns(InstallationPath, ReleaseName),
					   {ReleaseName, ReleaseVersions}
				   end, ReleaseNames))],
    FlatSpecs = fetch_flat_list_app_specs(InstallationPath, ReleaseTuples),
    TargetSpecs -- FlatSpecs.
    
    

fetch_flat_list_app_specs(InstallationPath, ReleaseTuples) ->
    lists:flatten(fetch_flat_list_app_specs2(InstallationPath, ReleaseTuples)).

fetch_flat_list_app_specs2(InstallationPath, [{RelName, RelVsns}|ReleaseTuples]) ->
    lists:map(fun(RelVsn) ->
		      case fetch_app_specs(InstallationPath, RelName, RelVsn) of
			  {ok, AppSpecs} -> AppSpecs;
			  _              -> []
		      end
	      end, RelVsns) ++ fetch_flat_list_app_specs2(InstallationPath, ReleaseTuples);
fetch_flat_list_app_specs2(_, []) ->
    [].

fetch_app_specs(InstallationPath, RelName, RelVsn) when is_atom(RelName) ->
    fetch_app_specs(InstallationPath, atom_to_list(RelName), RelVsn);
fetch_app_specs(InstallationPath, RelName, RelVsn) ->
    RelFilePath = fax_paths:installed_release_file_path(InstallationPath, RelName, RelVsn),
    case catch fax_util:consult_rel_file(RelFilePath) of
	{ok, {_ErtsVsn, AppSpecs}} -> {ok, AppSpecs};
	Error                     -> {error, {"could not find app specs for the release specified", Error}}
    end.
    

print_list(_Side, []) ->
    ok;
print_list(lib, List) ->
    print_list2("Applications (install with: faxien install_app)", List);
print_list(releases, List) ->
    print_list2("Releases (install with: faxien install)", List).

print_list2(Header, List) ->
    io:format("~s~n", [Header]),
    lists:foreach(fun(E) -> io:format("    ~s~n", [E]) end, List).
			  


filter(FilterFun, List) -> 
    SortedList = lists:sort(List),
    element(2, lists:foldl(fun(E, {Cur, IAcc} = Acc) -> 
				   case E == Cur of
				       true  -> 
					   Acc;
				       false -> 
					   case FilterFun(E) of
					       true  -> {E, [E|IAcc]};
					       false -> Acc
					   end
				   end
			   end, {undefined, []}, SortedList)).

raw_list(Repos, Side) ->
    lists:foldl(fun(Repo, Acc) -> 
			SysInfo  = ewr_util:system_info(),
			Suffixes = ewr_util:gen_multi_erts_repo_stub_suffix("", [SysInfo, "Generic"], Side),
			try
			    lists:foldl(fun(Suf, Acc2) -> 
						?INFO_MSG("pulling data for list from ~s~n", [Repo ++ "/" ++ Suf]),
						case fax_util:repo_list(Repo ++ "/" ++ Suf) of
						    {ok, Vsns}           -> [{Repo, lists:reverse(Vsns)}|Acc2]; 
						    {error, conn_failed} -> throw(conn_failed);
						    {error, _Reason}     -> Acc2
						end
					end, Acc, Suffixes)
			catch
				conn_failed ->
				       Acc
			end
		end, [], Repos).

%%--------------------------------------------------------------------
%% @private
%% @doc 
%%  Fetch the description for a particular application.
%% <pre>
%% Variables:
%%  VsnThreshold - is used to indicate that the highest version possible is to be installed but no higher than VsnThreshold.
%% </pre>
%% @spec describe_app(Repos, TargetErtsVsn, AppName, AppVsn, VsnThreshold, VsnThreshold) -> ok
%%  where
%%   Repos = list()
%%   TargetErtsVsn = string()
%%   AppName = string()
%%   AppVsn = string()
%%   VsnThreshold = string() | infinity
%% @todo Handle stepping down valid erts versions.
%% @end
%%--------------------------------------------------------------------
describe_app([], _TargetErtsVsn, _AppName, "LATEST", _VsnThreshold) ->
    {error, "could not find description"};
describe_app(Repos, TargetErtsVsn, AppName, "LATEST", VsnThreshold) ->
    {ok, {Repo, HighVsn}} = fax_util:find_highest_vsn(Repos, TargetErtsVsn, AppName, lib, VsnThreshold),
    ShortenedRepos        = lists:delete(Repo, Repos),
    case catch describe_app([Repo|ShortenedRepos], TargetErtsVsn, AppName, HighVsn) of
	ok -> 
	    ok;
	Error -> 
	    ?ERROR_MSG("failed with ~p for vsn ~p in repo ~p moving on to next repo and setting vsn threshold to ~p~n", 
		       [Error, HighVsn, Repo, VsnThreshold]), 
	    describe_app(ShortenedRepos, TargetErtsVsn, AppName, "LATEST", HighVsn)
    end.
